﻿using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using Sigil.Impl;

namespace Sigil
{
	public partial class Emit<DelegateType>
	{
		/// <summary>
		/// Pops a pointer from the stack and pushes the value (of the given type) at that address onto the stack.
		/// </summary>
		public Emit<DelegateType> LoadIndirect<Type>(Boolean isVolatile = false, Int32? unaligned = null)
		{
			return LoadIndirect(typeof(Type), isVolatile, unaligned);
		}

		/// <summary>
		/// Pops a pointer from the stack and pushes the value (of the given type) at that address onto the stack.
		/// </summary>
		public Emit<DelegateType> LoadIndirect(Type type, Boolean isVolatile = false, Int32? unaligned = null)
		{
			if (type == null)
			{
				throw new ArgumentNullException("type");
			}

			if (unaligned.HasValue && (unaligned != 1 && unaligned != 2 && unaligned != 4))
			{
				throw new ArgumentException("unaligned must be null, 1, 2, or 4");
			}

			OpCode? instr = null;
			IEnumerable<StackTransition> transitions = null;

			if (type.IsPointer)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { type }),
                        new StackTransition(new [] { type.MakePointerType() }, new[] { type }),
                        new StackTransition(new [] { type.MakeByRefType() }, new[] { type })
                    };

				instr = OpCodes.Ldind_I;
			}

			if (!type.IsValueType && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { type }),
                        new StackTransition(new [] { type.MakePointerType() }, new[] { type }),
                        new StackTransition(new [] { type.MakeByRefType() }, new[] { type })
                    };

				instr = OpCodes.Ldind_Ref;
			}

			if (type == typeof(sbyte) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(sbyte*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(sbyte).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_I1;
			}

			if (type == typeof(Boolean) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Boolean*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Boolean).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_I1;
			}

			if (type == typeof(Byte) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Byte*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Byte).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_U1;
			}

			if (type == typeof(Int16) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int16*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int16).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_I2;
			}

			if (type == typeof(UInt16) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(UInt16*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(UInt16).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_U2;
			}

			if (type == typeof(Char) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Char*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Char).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_U2;
			}

			if (type == typeof(Int32) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_I4;
			}

			if (type == typeof(UInt32) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(UInt32*) }, new[] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(UInt32).MakeByRefType() }, new[] { typeof(Int32) })
                    };

				instr = OpCodes.Ldind_U4;
			}

			if ((type == typeof(Int64) || type == typeof(UInt64)) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Int64) }),
                        new StackTransition(new [] { typeof(Int64*) }, new[] { typeof(Int64) }),
                        new StackTransition(new [] { typeof(Int64).MakeByRefType() }, new[] { typeof(Int64) }),
                        new StackTransition(new [] { typeof(UInt64*) }, new[] { typeof(Int64) }),
                        new StackTransition(new [] { typeof(UInt64).MakeByRefType() }, new[] { typeof(Int64) })
                    };

				instr = OpCodes.Ldind_I8;
			}

			if (type == typeof(Single) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Single) }),
                        new StackTransition(new [] { typeof(Single*) }, new[] { typeof(Single) }),
                        new StackTransition(new [] { typeof(Single).MakeByRefType() }, new[] { typeof(Single) })
                    };

				instr = OpCodes.Ldind_R4;
			}

			if (type == typeof(Double) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType) }, new [] { typeof(Double) }),
                        new StackTransition(new [] { typeof(Double*) }, new[] { typeof(Double) }),
                        new StackTransition(new [] { typeof(Double).MakeByRefType() }, new[] { typeof(Double) })
                    };

				instr = OpCodes.Ldind_R8;
			}

			if (!instr.HasValue)
			{
				throw new InvalidOperationException("LoadIndirect cannot be used with " + type + ", LoadObject may be more appropriate");
			}

			if (isVolatile)
			{
				UpdateState(OpCodes.Volatile, Wrap(StackTransition.None(), "LoadIndirect"));
			}

			if (unaligned.HasValue)
			{
				UpdateState(OpCodes.Unaligned, (Byte)unaligned.Value, Wrap(StackTransition.None(), "LoadIndirect"));
			}

			UpdateState(instr.Value, Wrap(transitions, "LoadIndirect"));

			return this;
		}
	}
}